'use strict';

const utils = require('./utils');

class WeixinPayBase {

	constructor(params) {
		this.options = params;
		
		if (!this.options) throw new Error('初始化参数不能为空')

		if (!this.options.appId) throw new Error('appId required')
		if (!this.options.mchId) throw new Error('mchId required')
		if (!this.options.v3Key) throw new Error('v3Key required')
		if (!this.options.appPrivateKeyPath && !this.options.appPrivateKeyContent) throw new Error('missing appPrivateKeyPath or appPrivateKeyContent')
		if (!this.options.appCertPath && !this.options.appCertContent) throw new Error('missing appCertPath or appCertContent')
		this.platformCertificate = [] // 平台证书
		this._baseURL = 'https://api.mch.weixin.qq.com'

		this._cert = this.options.appCertPath ? utils.loadCertFromPath(this.options.appCertPath) : utils
			.loadCertFromContent(
				utils.formatKey(this.options.appCertContent, 'CERTIFICATE')
			)
		// console.log('_cert', this._cert);
		
		this._privateKey = this.options.appPrivateKeyPath ? utils.loadPrivateKeyFromPath(this.options
			.appPrivateKeyPath) : utils.loadPrivateKeyFromContent(
			utils.formatKey(this.options.appPrivateKeyContent, 'PRIVATE KEY')
		)
		// console.log('_privateKey', this._privateKey);
	}

	/**
	 * 生成 Authorization
	 * @param {String} method 请求方式
	 * @param {String} action URL
	 * @param {Object} body 请求体
	 * @returns {string}
	 * @private
	 */
	_getAuthorization(method = 'GET', action = '', body = {}) {
		const nonceStr = utils.getNonceStr()
		const timestamp = Date.parse(new Date()) / 1000
		const signStr = [
			method,
			method === 'GET' && Object.keys(body).length > 0 ? `${action}?${utils.getQueryStr(body)}` : action,
			timestamp,
			nonceStr,
			method === 'GET' ? '' : JSON.stringify(body)
		].reduce((sign, key) => {
			sign += key + '\n'
			return sign
		}, '')

		const sign = utils.rsaPrivateKeySign(this._privateKey.toPEM(), signStr).toString('base64')
		const certSerialNo = this._cert.serialNumber.toUpperCase()
		
		const res = `WECHATPAY2-SHA256-RSA2048 mchid="${this.options.mchId}",nonce_str="${nonceStr}",signature="${sign}",timestamp="${timestamp}",serial_no="${certSerialNo}"`;
		
		// console.log('_getAuthorization', res);
		return res;
	}

	/**
	 * 请求
	 * @param action
	 * @param params
	 * @param method
	 * @param successStatusCode
	 * @returns {Promise<*>}
	 * @private
	 */
	async _request(action, params, method = 'GET', successStatusCode = 200) {
		const authorization = this._getAuthorization(method, action, params);
		let res = { };
		if (this.options.agent) {
			switch (method) {
				case 'GET':
					res = await uniCloud.httpProxyForEip.get(`${this._baseURL}${action}`, params, {
						Accept: 'application/json',
						'content-type': 'application/json',
						Authorization: authorization
					});
					break;
				case 'POST':
					res = await uniCloud.httpProxyForEip.postJson(`${this._baseURL}${action}`, params, {
						Accept: 'application/json',
						'content-type': 'application/json',
						Authorization: authorization
					});
					break;
			}
		} else {
			res = await uniCloud.httpclient.request(`${this._baseURL}${action}`, {
				method,
				data: params,
				headers: {
					Accept: 'application/json',
					'content-type': 'application/json',
					Authorization: authorization
				},
				dataType: 'json',
				timeout: this.options.timeout
			})
		}
		console.log('_request', action, params, res.data);
		let {
			status,
			data = {},
			headers
		} = res;
		
		if (res.body) {
			data = res.body;
		}
		
		if (res.statusCode) {
			status = res.statusCode;
		} else if (res.statusCodeValue) {
			status = res.statusCodeValue;
		}
		
		if (status !== successStatusCode) {
			// throw new Error(data.message);
			return data;
		}
		
		if (!data) data = {}
		// 响应签名验证
		// await this._verifyResponseSign(headers, data)

		if (data.appid) data.appId = data.appid
		if (data.mchid) data.mchId = data.mchid

		return data
	}

	/**
	 * 请求公共参数
	 * @param params
	 * @returns {any}
	 * @private
	 */
	_publicParams(params) {
		const publicParams = {
			appid: this.options.appId,
			mchid: this.options.mchId
		}

		return Object.assign(publicParams, params)
	}

	/**
	 * 生成客户端支付参数
	 * @param {String} prepayId
	 * @param {String} tradeType
	 * @returns {{package: string, appid: *, partnerid: (String|*), prepayid, noncestr: string, timestamp: string}|{timeStamp: string, package: string, appId: *, nonceStr: string}}
	 * @private
	 */
	_getPayParamsByPrepayId(prepayId, tradeType) {
		let requestOptions
		// 请务必注意各个参数的大小写
		switch (tradeType) {
			case 'APP':
				requestOptions = {
					appid: this.options.subAppId ? this.options.subAppId : this.options.appId,
					partnerid: this.options.mchId,
					prepayid: prepayId,
					package: 'Sign=WXPay',
					noncestr: utils.getNonceStr(),
					timestamp: '' + ((Date.now() / 1000) | 0)
				}
				requestOptions.sign = this._clientPaySign(requestOptions, tradeType)
				break
			case 'JSAPI':
			default: {
				const timeStamp = '' + ((Date.now() / 1000) | 0)
				requestOptions = {
					appId: this.options.subAppId ? this.options.subAppId : this.options.appId,
					nonceStr: utils.getNonceStr(),
					package: 'prepay_id=' + prepayId,
					timeStamp
				}
				// signType也需要sign
				requestOptions.signType = 'RSA'
				requestOptions.paySign = this._clientPaySign(requestOptions, tradeType)
				requestOptions.timestamp = timeStamp
				break
			}
		}
		return requestOptions
	}

	/**
	 * 生成客户端支付签名
	 * @param {Object} params
	 * @param {String} tradeType
	 * @returns {string}
	 * @private
	 */
	_clientPaySign(params, tradeType) {
		const signStr = [
			params.appid || params.appId,
			params.timestamp || params.timeStamp,
			params.noncestr || params.nonceStr,
			tradeType === 'JSAPI' ? params.package : params.prepayid
		].reduce((sign, key) => {
			sign += key + '\n'
			return sign
		}, '')

		return utils.rsaPrivateKeySign(this._privateKey.toPEM(), signStr).toString('base64')
	}


	/**
	 * 获取微信支付平台证书
	 * 调用频率限制 1000次/s
	 * @see https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
	 * @returns {Promise<*|*[]>}
	 * @private
	 */
	async _getPlatformCert() {
		if (this.platformCertificate.length <= 0) {
			const action = '/v3/certificates'
			const {
				status,
				data
			} = await uniCloud.httpclient.request(`${this._baseURL}${action}`, {
				method: 'GET',
				headers: {
					Accept: 'application/json',
					'content-type': 'application/json',
					Authorization: this._getAuthorization('GET', action)
				},
				dataType: 'json',
				timeout: this.options.timeout
			})
			
			console.log(action, data, status);
			
			if (status !== 200) {
				throw new Error('request fail ' + data.message);
			}
			
			this.platformCertificate = data.data.reduce((res, cert) => {
				// 解密证书
				if (cert.encrypt_certificate) {
					const {
						nonce,
						associated_data: associatedData,
						ciphertext
					} = cert.encrypt_certificate;
					
					const certContent = utils.decryptCiphertext(ciphertext, this.options.v3Key, nonce, associatedData)
					cert.certificate = utils.loadCertFromContent(certContent)
				}
				
				res.push(cert);
				return res;
			}, []) || [];
		}

		this.platformCertificate = this.platformCertificate.filter(item => new Date(item.expire_time).getTime() > Date.now());
		return this.platformCertificate[0];
	}


	/**
	 * 验证响应签名
	 * @param {Object} headers
	 * @param {Object} data
	 * @returns {Promise<void>}
	 * @private
	 */
	async _verifyResponseSign(headers, data = {}) {
		// 获取平台证书
		const platformCert = await this._getPlatformCert();

		let {
			'wechatpay-timestamp': wechatPayTimestamp,
			'wechatpay-nonce': wechatPayNonce,
			'wechatpay-signature': wechatPaySignature
		} = headers;
		
		if (!wechatPaySignature) {
			wechatPaySignature = headers['Wechatpay-Signature'];
			wechatPayNonce = headers['Wechatpay-Nonce'];
			wechatPayTimestamp = headers['Wechatpay-Timestamp'];
		}

		const signStr = [
			wechatPayTimestamp,
			wechatPayNonce,
			Object.keys(data).length ? JSON.stringify(data) : ''
		].reduce((sign, key) => {
			sign += key + '\n'
			return sign
		}, '');

		const verify = utils.rsaPublicKeyVerifySign(platformCert.certificate.publicKey.toPEM(), signStr, wechatPaySignature);
		if (!verify) throw new Error('response signature verification failed');
	}


	/**
	 * 下载账单文件
	 * @param {String} fileUrl 下载地址
	 * @returns {*}
	 * @private
	 */
	_downloadFile(fileUrl) {
		const url = require('url');
		const urlParser = url.parse(fileUrl)
		return uniCloud.httpclient.request(fileUrl, {
			method: 'GET',
			headers: {
				Accept: 'application/json',
				'content-type': 'application/json',
				Authorization: this._getAuthorization('GET', urlParser.path)
			},
			dataType: 'text',
			timeout: this.options.timeout
		})
	}
}

class WeixinPayV3 extends WeixinPayBase {
	
	async getPlatformCert() {
		return await this._getPlatformCert();
	}
	
	/**
	 * 生成下单参数
	 * @param {String} params.openid
	 * @param {String} params.body
	 * @param {String} params.outTradeNo
	 * @param {Number} params.totalFee
	 * @param {String} params.notifyUrl
	 * @param {String} params.spbillCreateIp
	 * @param {Object} params.sceneInfo
	 * @param {String} params.tradeType
	 * @returns {Promise<*>}
	 */
	async getOrderInfo(params) {
		params = this._publicParams(params)
		params.sceneInfo.payerClientIp = params.sceneInfo.payerClientIp || '127.0.0.1'
		if (params.tradeType !== 'JSAPI') {
			delete params.openid
		}

		if (!params.tradeType) throw new Error('tradeType required')

		const {
			tradeType,
			...rest
		} = params

		// 兼容微信tradeType
		const orderResult = await this._request(
			`/v3/pay/transactions/${params.tradeType === 'MWEB' ? 'h5' : params.tradeType.toLowerCase()}`, rest,
			'POST')

		if (params.tradeType === 'NATIVE' || params.tradeType === 'MWEB') {
			return orderResult
		}

		if (!orderResult.prepayId) {
			throw new Error(orderResult.errMsg || '获取prepayId失败')
		}

		return this._getPayParamsByPrepayId(orderResult.prepayId, params.tradeType)
	}

	/**
	 * 查询订单
	 * @param {String} params.transactionId 平台订单号
	 * @param {String} params.outTradeNo 商户订单号
	 * @returns {Promise<*>}
	 */
	async orderQuery(params) {
		const res = await this._request(
			params.transactionId ?
			`/v3/pay/transactions/id/${params.transactionId}` :
			`/v3/pay/transactions/out-trade-no/${params.outTradeNo}`, {
				mchid: this.options.mchId
			})

		// 手动计算 应结订单金额 （应结订单金额=订单金额-免充值优惠券金额）
		res.settlementTotalFee = 0
		if (res.promotion_detail && res.promotion_detail.length > 0) {
			const free = res.promotion_detail.reduce((amount, coupon) => {
				if (coupon.type === 'NOCASH') {
					amount += coupon.amount
				}

				return amount
			}, 0)

			res.settlementTotalFeeres = res.amount.total - free
		}
		return res
	}

	/**
	 * 关闭订单
	 * @param {String} params.outTradeNo 商户订单号
	 * @returns {Promise<*>}
	 */
	async closeOrder(params) {
		return await this._request(`/v3/pay/transactions/out-trade-no/${params.outTradeNo}/close`, {
			mchid: this.options.mchId
		}, 'POST', 204)
	}

	/**
	 * 申请退款
	 * @param {String} params.outTradeNo 商户订单号
	 * @param {String} params.transactionId 平台订单号
	 * @param {String} params.outRefundNo 商户退款单号
	 * @param {String} params.totalFee 订单总金额
	 * @param {String} params.refundFee 退款总金额
	 * @param {String} params.refundFeeType 货币种类
	 * @param {String} params.refundDesc 退款原因
	 * @param {String} params.notifyUrl 退款通知 url
	 * @returns {Promise<*>}
	 */
	async refund(params) {
		return await this._request('/v3/refund/domestic/refunds', params, 'POST')
	}

	/**
	 * 单笔退款查询
	 * @description 响应与v2差异: 增加字段: refundId(平台退款单号)，缺少字段：refundDesc(退款理由)，refundList(分笔退款信息)
	 * @param {String} params.outTradeNo 商户订单号
	 * @returns {Promise<*>}
	 */
	async refundQuery(params) {
		return await this._request(`/v3/refund/domestic/refunds/${params.outRefundNo}`)
	}

	/**
	 * 申请交易账单
	 * @param {String} params.billDate 下载对账单的日期，格式：2014-06-03
	 * @param {String} params.billType
	 * @returns {Promise<*>}
	 */
	async downloadBill(params) {
		return this._request('/v3/bill/tradebill', params).then(res => {
			return this._downloadFile(res.downloadUrl)
		}).then(response => {
			return Promise.resolve({
				content: response.data
			})
		})
	}

	/**
	 * 申请资金账单
	 * @description 请求与V2差异：billDate(下载对账单的日期) 格式变更为 2014-06-03
	 * @param {String} params.billDate 下载对账单的日期，格式：2014-06-03
	 * @param {String} params.accountType
	 * @returns {Promise<*>}
	 */
	async downloadFundflow(params) {
		return this._request('/v3/bill/fundflowbill', params).then(res => {
			return this._downloadFile(res.downloadUrl)
		}).then(response => {
			return Promise.resolve({
				content: response.data
			})
		})
	}

	/**
	 * 获取通知类型
	 * @param event
	 * @returns {String}
	 */
	async checkNotifyType(event) {
		const {
			headers
		} = event
		const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body
		// 请求合法验证
		await this._verifyResponseSign(headers, body)

		const {
			resource = {}
		} = body

		switch (resource.original_type) {
			case 'transaction':
			default:
				return 'payment'
			case 'refund':
				return 'refund'
		}
	}

	/**
	 * 支付成功回调通知
	 * @param event
	 * @returns {Promise<*>}
	 */
	async verifyPaymentNotify(event) {
		const {
			headers
		} = event
		const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body

		// 请求合法验证
		await this._verifyResponseSign(headers, body)

		const {
			resource
		} = body
		const decrypted = utils.decryptCiphertext(resource.ciphertext, this.options.v3Key, resource.nonce, resource
			.associated_data)

		return JSON.parse(decrypted);
	}

	/**
	 * 退款回调通知
	 * @param event
	 * @returns {Promise<*>}
	 */
	async verifyRefundNotify(event) {
		const {
			headers
		} = event
		const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body

		// 请求合法验证
		await this._verifyResponseSign(headers, body)

		const {
			resource
		} = body
		const decrypted = utils.decryptCiphertext(resource.ciphertext, this.options.v3Key, resource.nonce, resource
			.associated_data)

		return JSON.parse(decrypted);
	}
	
	/**
	 * 发起转账 - 商家转账到零钱
	 * https://pay.weixin.qq.com/docs/merchant/apis/batch-transfer-to-balance/transfer-batch/initiate-batch-transfer.html
	 * @returns {Promise<*>}
	 */
	async transferBatches(params) {
		const body = {
			appid: this.options.appId,
			...params
		};
		
		const res = await this._request(`/v3/transfer/batches`, body, 'POST');
	
		return res;
	}
	
	/**
	 * 查询批次单
	 * https://pay.weixin.qq.com/docs/merchant/apis/batch-transfer-to-balance/transfer-batch/get-transfer-batch-by-out-no.html
	 */
	async transferBatchesQuery(params) {
		if (!params) {
			throw new Error('请提供批次单号');
		}
		if (!(params.batch_id || params.out_batch_no)) {
			throw new Error('请提供微信批次单号或商家批次单号');
		}
		
		let action = `/v3/transfer/batches/batch-id/${params.batch_id}`;
		if (params.out_batch_no) {
			action = `/v3/transfer/batches/out-batch-no/${params.out_batch_no}`;
		}
		
		const res = await this._request(action, { need_query_detail: params.need_query_detail || false });
			
		return res;
	}
	
}

module.exports = WeixinPayV3;